package neuralmusic.brain;

import java.util.TreeSet;
import java.util.concurrent.TimeUnit;

import neuralmusic.brain.module.BasicDelayed;
import neuralmusic.brain.module.Scheduler;
import sun.reflect.generics.reflectiveObjects.NotImplementedException;
import uk.org.toot.audio.core.AudioBuffer;
import uk.org.toot.audio.core.AudioProcess;

public class AudioThreadScheduler extends Scheduler implements AudioScheduler {

	private static final double END_OF_TIME = Double.MAX_VALUE;

	private double currentTime = 0.0;
	double when;
	double T;
	TreeSet<BasicDelayed> treeNormal = new TreeSet<BasicDelayed>();

	TreeSet<AudioEvent> treeAudio = new TreeSet<AudioEvent>();

	OscillatorBank client;

	public AudioThreadScheduler(OscillatorBank bank) {
		proccess = new SlaveProcess();
		client = bank;
	}

	@Override
	public double getCurrentTimeInSecs() {

		return currentTime;
	}

	Thread blockThread;

	private AudioProcess proccess;

	// private Thread waiter;

	void nudge() {
		if (blockThread != null) {
			blockThread.notify();
		}

	}

	@Override
	synchronized public BasicDelayed take() {

		blockThread = Thread.currentThread();
		BasicDelayed n = null;
		while (blockThread != null) {
			if (!treeNormal.isEmpty())
				n = treeNormal.first();

			if (n != null)
				when = n.getWhen();
			else
				when = END_OF_TIME;
			if (when > currentTime) {
				try {
					wait();
				} catch (InterruptedException e) {
					// System.out.println(" Nudged "+ currentTime);
				}
			} else {
				blockThread = null;
			}
		}

		treeNormal.remove(n);
		return n;
	}

	@Override
	public void add(BasicDelayed neuron) {
		treeNormal.add(neuron);
	}

	@Override
	public long getDelay(TimeUnit unit, double triggerInSecs) {
		throw new NotImplementedException();
	}

	@Override
	public int size() {
		return treeNormal.size();
	}

	public AudioProcess getAudioProcess() {

		return proccess;
	}

	double getNextAudioTime() {
		if (treeAudio.isEmpty())
			return Double.MAX_VALUE;
		AudioEvent ev = treeAudio.first();
		return ev.getWhen();
	}

	class SlaveProcess implements AudioProcess {

		// private AudioProcess client;
		private AudioBuffer nible;
		private float fs;
		private int chunkSize;
		private float dtChunk;
		private float[] nibBuff;

		@Override
		public void close() throws Exception {
			// TODO Auto-generated method stub

		}

		@Override
		public void open() throws Exception {
			// TODO Auto-generated method stub

		}

		int cccc = 0;

		/*
		 * Called from the audioServer with a fixed chunk size (non-Javadoc)
		 * breaks up chunk to service the required events from the scheduler.
		 */
		@Override
		public int processAudio(AudioBuffer chunk) {

			// client.processAudio(chunk);
			// if (true) return AUDIO_OK;

			// System.out.println(" PA:1 t="+currentTime + "  que ="
			// +treeAudio.size());
			try {
				int sampCheck = 0;

				if (nible == null) {
					fs = chunk.getSampleRate();
					T = 1.0 / fs;
					chunkSize = chunk.getSampleCount();
					nible = new AudioBuffer("nibble", 1, chunkSize, fs);
					dtChunk = chunkSize / fs;
					nibBuff = nible.getChannel(0);

				}

				processPendingAudioEvents();

				// first event.when>currentTime

				// System.out.println(" PA:1A    que =" +treeAudio.size());

				float chn[] = chunk.getChannel(0);

				/* when should be the time of next event on the queue */

				double nextAudioTime = getNextAudioTime();
				double dt = nextAudioTime - currentTime;  // Add T to make sure we get event
				double endOfChunkTime = currentTime + dtChunk;

				if (dt > dtChunk) {
					// System.out.println(" PA:2 DT="+dt);
					client.processAudio(chunk);
					currentTime += dtChunk;
					sampCheck += chunkSize;
				} else {
					// System.out.println(" PA:3");
					int ptr = 0;
					boolean done = false; // set done when we have no more
											// events in
					int nibSize1 = (int) (dt * fs); // the chunk period
					while (true) {
						// System.out.println(" PA:4  DT="+dt);

						assert (dt >= 0);
						assert (nibSize1 > 0);
						assert (nibSize1 < chunkSize);
						nible.changeSampleCount(nibSize1, false);
						nible.makeSilence();
						client.processAudio(nible);
						nibBuff = nible.getChannel(0);
						for (int i = 0; i < nibSize1; i++, ptr++) {
							chn[ptr] += nibBuff[i];
						}

						assert (nibBuff == nible.getChannel(0));

						sampCheck += nibSize1;

						if (done) {
							currentTime = endOfChunkTime;
							break;
						}

						currentTime = nextAudioTime;
						processPendingAudioEvents();
						nextAudioTime = getNextAudioTime();

						if (nextAudioTime == Double.MAX_VALUE) {
							nibSize1 = chunkSize - ptr;
							assert (nibSize1 != 0);
							done = true;
						} else {
							dt = nextAudioTime - currentTime;
							nibSize1 = (int) (dt * fs);

							if (ptr + nibSize1 >= chunkSize) {
								nibSize1 = chunkSize - ptr;
								//if (nibSize1 == 0 ) break;
								assert (nibSize1 >= 0);
								done = true;
							}
						}
					}
				}
				
				if (chunk.getChannelCount() == 2) {
					System.arraycopy(chn, 0, chunk.getChannel(1), 0, chunkSize);
				}
			
				cccc++;
				assert (Math.abs(currentTime - cccc * dtChunk) < T);
			} catch (AssertionError e) {
				e.printStackTrace();
			}
			return AUDIO_OK;
		}

		private void processPendingAudioEvents() {

			while (!treeAudio.isEmpty()) {

				AudioEvent ev = treeAudio.first();
				if (ev.getWhen() > currentTime + T)
					return;
				// System.out.println(" fire event now=" + currentTime);
				// System.out.println(" fire event when=" + ev.getWhen()
				// + " **************************************** ");
				treeAudio.remove(ev); // important to remove before firing
				ev.fire(AudioThreadScheduler.this);
			}
		}

	}

	@Override
	public void addAudioEvent(AudioEvent ev) {
		// System.out.println(" add event " + ev.getWhen());
		synchronized (treeAudio) {
			treeAudio.add(ev);
		}

	}
}